package com.yyoo.doc.html.sax;

import com.yyoo.doc.html.style.css.CssParser;
import com.yyoo.doc.html.tag.HtmlTag;
import com.yyoo.doc.mypdf.MyPdfException;
import lombok.Getter;
import lombok.Setter;
import org.xml.sax.Attributes;
import org.xml.sax.helpers.DefaultHandler;

import java.net.URI;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;

/**
 * @author zzy
 * html 的 SAX 解析器抽象类
 */
@Getter
@Setter
public abstract class AbsSaxHandler extends DefaultHandler {

    /**
     * 字符编码，默认 UTF-8
     */
    private Charset charSet = StandardCharsets.UTF_8;

    /**
     * 当前文档的根路径（用于解析html 的外部链接，如 link 的外部 css，image的src路径）
     */
    private String baseUri;

    /**
     * 当前文档的内部样式和外部样式装载后的键值对
     * Map<选择器,Map<属性,值>>
     */
    private Map<String, Map<String,String>> cssStyleMap = new HashMap<>();

    /**
     * 当前正在解析的 html 元素
     */
    private LinkedList<SaxHtmlElement> htmlElStack;

    /**
     * 是否跳过当前元素解析
     */
    protected boolean ignore = false;

    /**
     * 当前标签解析的元素
     */
    private SaxHtmlElement currentElement;


    /**
     * SAX 解析器构造器
     */
    protected AbsSaxHandler(String baseUri){
        if(baseUri != null){
            baseUri = baseUri.replace("\\", "/");
        }
        this.baseUri = baseUri;
        this.htmlElStack = new LinkedList<>();
    }


    @Override
    public void startElement(String uri, String localName, String qName, Attributes attributes) {
        qName = qName.toLowerCase();

        // 如果当前标签不支持解析
        if(HtmlTag.isIgnoreTag(qName)){
            ignore = true;
            return;
        }else {
            ignore = false;
        }

        this.currentElement = createHtmlElement(qName, attributes,null,baseUri);
        htmlElStack.add(this.currentElement);

        if(HtmlTag.LINK.equals(qName)){
            String href = attributes.getValue("href");
            loadCssFile(href);
            htmlElStack.pollLast();
            return;
        }

       start();

    }

    @Override
    public void endElement(String uri, String localName, String qName){

        // endElement 中不能使用 this.currentElement
        qName = qName.toLowerCase();
        // 如果当前标签不支持解析且和 link 与 page_new 一样，在 startElement 中已经处理 则跳过
        if(HtmlTag.isIgnoreTag(qName) || HtmlTag.PAGE_NEW.equals(qName) || HtmlTag.LINK.equals(qName)){
            ignore = true;
            return;
        }else {
            ignore = false;
        }

        SaxHtmlElement curEl = htmlElStack.pollLast();
        if(curEl == null){
            return;
        }
        if (HtmlTag.STYLE.equalsIgnoreCase(curEl.getTagName())) {
            loadCssStyle(curEl.getOwnTextString());
            return;
        }

        end(curEl);
    }

    /**
     * 创建一个 html 元素对象
     *
     * @param tagName    标签名
     * @param attributes 标签属性
     * @param ownText 标签文本
     * @param baseUri    当前文档的根路径
     * @return SaxHtmlElement 对象
     */
    public SaxHtmlElement createHtmlElement(String tagName, Attributes attributes, StringBuilder ownText, String baseUri){
        return new SaxHtmlElement(tagName,attributes,ownText,baseUri);
    }

    /**
     * 开始解析
     * 进入此方法时，currentElement 已经被设置，并已经添加到 htmlElStack 中（入栈）
     */
    public abstract void start();

    /**
     * 结束解析
     * @param curEl 当前解析的 html 元素
     * curEl 是从 htmlElStack 中取出来的最后一个元素（出栈）
     */
    public abstract void end(SaxHtmlElement curEl);

    /**
     * 注意：characters 并不是一次读取该标签的所有字符（可能会读取多次）
     * @param ch The characters.
     * @param start The start position in the character array.
     * @param length The number of characters to use from the
     *               character array.
     */
    @Override
    public void characters(char[] ch, int start, int length) {
        if (ignore) {
            return;
        }

        String content = new String(ch, start, length);
        if (content.trim().isEmpty()) {
            return;
        }

        StringBuilder buf = getStringBuilder(content);
        this.currentElement.appendText(buf.toString());

    }

    /**
     * 将字符串除去空格等
     * @param content 标签对应的文本内容
     */
    private StringBuilder getStringBuilder(String content) {
        StringBuilder buf = new StringBuilder();
        int len = content.length();
        char character;
        boolean newline = false;
        for (int i = 0; i < len; i++) {
            character = content.charAt(i);
            switch (character) {
                case ' ':
                    if (!newline) {
                        buf.append(character);
                    }
                    break;
                case '\n':
                    if (i > 0) {
                        newline = true;
                        buf.append(' ');
                    }
                    break;
                case '\r':
                case '\t':
                    break;
                default:
                    newline = false;
                    buf.append(character);
            }
        }
        return buf;
    }

    /**
     * 加载 html 文档中的 css 样式
     * link 的外部样式 和 style 标签的内部样式
     */
    protected void loadCssFile(String href){
        CssParser cssParser = new CssParser();
        cssParser.setCharSet(this.charSet);

        // 解析外部 css 文件
        try {
            URI p = new URI(getBaseUri());
            URI cssUrl = p.resolve(href);

            Map<String, Map<String, String>> styleMap = null;
            if (!cssUrl.toString().startsWith("http")) {
                // 如果不是 http 或 https 开头的，则认为是本地文件
                styleMap = cssParser.parse(Paths.get(cssUrl.getPath()));
            } else {
                styleMap = cssParser.parse(cssUrl.toURL());
            }

            if (styleMap != null) {
                this.cssStyleMap.putAll(styleMap);
            }
        } catch (Exception e) {
            throw new MyPdfException("", e);
        }
    }

    /**
     * 加载 style 标签内部样式
     * @param cssStyle 样式字符串
     */
    protected void loadCssStyle(String cssStyle){
        CssParser cssParser = new CssParser();
        cssParser.setCharSet(this.charSet);
        if(cssStyle != null && !cssStyle.isEmpty()) {
            Map<String, Map<String, String>> styleMap = cssParser.parse(cssStyle);
            this.cssStyleMap.putAll(styleMap);
        }
    }

    /**
     * 构造获取当前标签的样式
     * @param el 当前标签对象
     * @return 当前标签的样式键值对
     */
    protected Map<String,String> getCssStyle(SaxHtmlElement el){
        Map<String,String> cssStyle = new HashMap<>();

        // 加载 元素 选择器样式
        String tag = el.getTagName();
        if(this.cssStyleMap.containsKey(tag)){
            cssStyle.putAll(this.cssStyleMap.get(tag));
        }

        // 加载 class 选择器样式
        String classAttr = el.getAttributeValue("class");
        if(classAttr != null){
            String[] classes = classAttr.split(" ");
            for (String clazz : classes) {
                if(this.cssStyleMap.containsKey("."+clazz)){
                    cssStyle.putAll(this.cssStyleMap.get("."+clazz));
                }
            }
        }

        // 加载 id 选择器样式
        String idAttr = el.getAttributeValue("id");
        if(idAttr != null){
            String id = "#"+idAttr;
            if(this.cssStyleMap.containsKey(id)){
                cssStyle.putAll(this.cssStyleMap.get(id));
            }
        }
        return cssStyle;
    }

}
